/**
 * Copyright 2008 Adam Ruggles.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package kiff.util.parser;

import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.util.LinkedList;
import java.util.Queue;

/**
 * Tokenizes a character stream for parsing bbcode.
 * @author Adam
 * @version $Id$
 * 
 * Created on Dec 13, 2008 at 7:52:47 PM 
 */
public class BBCodeTokenizer implements Tokenizer {
	/**
	 * The input stream for the tokenizer to work on.
	 */
	protected Reader reader;

	/**
	 * The character that was read from the last next operation.
	 */
	private int character;

	/**
	 * A Queue for storing tokens.
	 */
	private Queue<Token> queue = new LinkedList<Token>();

	/**
	 * A StringBuilder to use as a character buffer.
	 */
	private StringBuilder buffer = new StringBuilder();

	/**
	 * Constructs a Tokenizer with a Reader.
	 * @param stream a character stream.
	 */
	public BBCodeTokenizer(final Reader stream) {
		this.reader = stream;
	}

	/**
	 * Constructs a Tokenizer with a String.
	 * @param stream a string.
	 */
	public BBCodeTokenizer(final String stream) {
		this.reader = new StringReader(stream);
	}

	/**
	 * Reads the next character in the stream.
	 * @throws IOException If an I/O error occurs.
	 */
	private int next() throws IOException {
		this.character = reader.read();
		return character;
	}

	/**
	 * Retrieves the next character in the stream and matches it against the input character.
	 * @param character The input character to match.
	 * @return True if the next character in the stream matches the input character.
	 * @throws IOException If an I/O error occurs.
	 */
	private boolean next(final int character) throws IOException {
		this.next();
		if (this.character != character) {
			return false;
		}
		return true;
	}

	/**
	 * Returns the next token in the character stream.
	 * @return The next token in the character stream.
	 * @throws IOException If an I/O error occurs.
	 */
	public Token nextToken() throws IOException {
		if (queue.peek() != null) {
			return queue.remove();
		}
		int scan = next();
		if (scan == '[') {
			return matchTag();
		}
		if (Character.isLetter(scan) || Character.isDigit(scan)) {
			buffer.append((char) scan);
			while (next() != Type.EOF) {
				if (!Character.isLetter(character) && !Character.isDigit(character)) {
					Token token = new Word(buffer.toString());
					buffer.setLength(0);
					if (character == '[') {
						queue.offer(matchTag());
					} else {
						queue.offer(new Token(character));
					}
					return token;
				}
				buffer.append((char) character);
			}
			queue.offer(new Token(character));
			Token token = new Word(buffer.toString());
			buffer.setLength(0);
			return token;
		}
		return new Token(scan);
		
	}

	/**
	 * Attempts to match the characters to a tag.
	 * @return The next token in the input stream.  If the stream matches a start or end tag the tag token
	 * will be returned, otherwise a Word or character token will be returned.
	 * @throws IOException If an I/O error occurs.
	 */
	private Token matchTag() throws IOException {
		if (next('/')) {
			// End Tag.
			while (next() != Type.EOF) {
				if (!Character.isLetter(character) && !Character.isDigit(character)) {
					break;
				}
				buffer.append((char) character);
			}
			if (character == ']') {
				Token token = new Tag(buffer.toString(), null, Type.END_TAG);
				buffer.setLength(0);
				return token;
			} else {
				queue.offer(new Token('/'));
				queue.offer(new Word(buffer.toString()));
				queue.offer(new Token(character));
				buffer.setLength(0);
				return new Token('[');
			}
		} else {
			String tagName = null;
			// Start Tag.
			while (character != Type.EOF) {
				if (!Character.isLetter(character) && !Character.isDigit(character)) {
					// Test character and change state.
					if (tagName == null) {
						if (character == '=') {
							tagName = buffer.toString();
							buffer.setLength(0);
						} else if (character == ']') {
							Token token = new Tag(buffer.toString(), null, Type.START_TAG);
							buffer.setLength(0);
							return token;
						} else {
							queue.offer(new Word(buffer.toString()));
							queue.offer(new Token(character));
							buffer.setLength(0);
							return new Token('[');
						}
					} else {
						if (character == ']') {
							Token token = new Tag(tagName, buffer.substring(1), Type.START_TAG);
							buffer.setLength(0);
							return token;
						} else if (character != ' ') {
							queue.offer(new Word(buffer.toString()));
							queue.offer(new Token(character));
							buffer.setLength(0);
							return new Token('[');
						}
					}
				}
				buffer.append((char) character);
				next();
			}
		}
		if (buffer.length() > 0) {
			queue.offer(new Token(character));
			Token token = new Word(buffer.toString());
			buffer.setLength(0);
			return token;
		}
		return new Token(character);
	}
}
